#include "limbo/ethernet/state.hpp"
#include "test-utils.h"

using namespace limbo;

uint8_t example_raw[]{
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x48, 0xba, 0x4e, 0x51, 0x39, 0xbb,
    0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x48, 0xba,
    0x4e, 0x51, 0x39, 0xbb, 0xc0, 0xa8, 0x65, 0x01, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0xc0, 0xa8, 0x65, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
auto example_chunk = Chunk(example_raw, sizeof(example_raw));
uint8_t crc_example[] = {0x00, 0x4a, 0x39, 0x02};

auto payload_raw = example_raw + 6 + 6 + 2;
auto payload_chunk = Chunk(payload_raw, example_chunk.size() - (6 + 6 + 2));

auto src = ethernet::MacAddress(example_raw + 6);
auto dst = ethernet::MacAddress(example_raw);

using State = ethernet::State<void>;
using Context = typename State::Context;
using Packet = typename State::Packet;

TEST_CASE("send", "[ethernet]") {
  char buff[sizeof(example_raw)];
  auto buff_chunk = Chunk(buff, sizeof(example_raw));
  auto ctx = Context{src, dst, ethernet::EtherType::arp, nullptr};
  auto state = State();

  SECTION("no enough space") {
    SECTION("zero buff") {
      auto zero_chunk = Chunk(buff, 0);
      auto result = Packet::send(state, ctx, zero_chunk, payload_chunk);
      REQUIRE(!result);
      CHECK(result.state() == &state);
      CHECK(result.error_code() == (uint32_t)Errc::no_enough_space);
    }
    SECTION("small buff") {
      auto zero_chunk = Chunk(buff, 63 - 4);
      auto result = Packet::send(state, ctx, zero_chunk, payload_chunk);
      REQUIRE(!result);
      CHECK(result.state() == &state);
      CHECK(result.error_code() == (uint32_t)Errc::no_enough_space);
    }
  }

  SECTION("normal payload") {
    auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
    REQUIRE(result);
    auto res_buff = result.consumed();
    CHECK((void *)res_buff.data() == (void *)buff);
    CHECK(res_buff.size() == example_chunk.size());
    CHECK(res_buff == example_chunk);
  }

  SECTION("small payload") {
    auto small_payload = Chunk(buff + payload_chunk.size() - 5, 5);
    auto result = Packet::send(state, ctx, buff_chunk, small_payload);
    REQUIRE(result);
    auto res_buff = result.consumed();
    CHECK((void *)res_buff.data() == (void *)buff);
    CHECK(res_buff.size() == 60);
  }

  SECTION("unknown dest") {
    ctx.destination = ethernet::MacAddress::unknown;
    auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
    REQUIRE(!result);
    CHECK(result.state() == &state);
    CHECK(result.error_code() == (uint32_t)Errc::ethernet_unknown_mac);
  }
}

TEST_CASE("recv/success", "[ethernet]") {
  auto state = State();
  auto result = state.recv(example_chunk, nullptr);
  CHECK(result);
  CHECK(result.consumed() == example_chunk);
  CHECK(result.state() == &state);
  auto &packet = state.get_parsed();
  CHECK(packet.source == src);
  CHECK(packet.destination == dst);
  CHECK(packet.type == (uint16_t)ethernet::EtherType::arp);
  CHECK(packet.crc == 0);
  CHECK(packet.payload == payload_chunk);
}

TEST_CASE("recv/incomplete", "[ethernet]") {
  auto state = State();
  auto chunk = Chunk(example_raw, 59);
  auto result = state.recv(chunk, nullptr);
  REQUIRE(!result);
  CHECK(result.state() == &state);
  CHECK(result.error_code() == (uint32_t)Errc::ethernet_malformed_frame);
  CHECK(result.consumed() == chunk);
}

TEST_CASE("crc32", "[ethernet]") {
  using O = ethernet::Opts;
  auto constexpr opts = ((int)O::calculate_crc) | ((int)O::validate_crc);
  using State = ethernet::State<void, opts>;
  using Context = typename State::Context;
  using Packet = typename State::Packet;

  char example_with_crc[sizeof(example_raw) + 4];
  memcpy(example_with_crc, example_raw, example_chunk.size());
  memcpy(example_with_crc + example_chunk.size(), crc_example, 4);
  auto example_crc = Chunk(example_with_crc, sizeof(example_with_crc));

  SECTION("send") {
    char buff[sizeof(example_raw) + 4];
    auto buff_chunk = Chunk(buff, sizeof(buff));
    auto ctx = Context{src, dst, ethernet::EtherType::arp, nullptr};
    auto state = State();
    auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
    REQUIRE(result);
    auto res_buff = result.consumed();
    CHECK((void *)res_buff.data() == (void *)buff);
    CHECK(res_buff.size() == example_crc.size());
    CHECK(res_buff == example_crc);
  };

  SECTION("recv") {
    auto state = State();
    auto result = state.recv(example_crc, nullptr);
    CHECK(result);
    CHECK(result.consumed() == example_crc);
    CHECK(result.state() == &state);
    auto &packet = state.get_parsed();
    CHECK(packet.source == src);
    CHECK(packet.destination == dst);
    CHECK(packet.type == (uint16_t)ethernet::EtherType::arp);
    CHECK(packet.crc == 0x2394a00);
    CHECK(packet.payload == payload_chunk);
  }

  SECTION("recv/crc mismatch") {
    auto state = State();
    memset(example_with_crc + example_crc.size() - 4, 0, 4);
    auto result = state.recv(example_crc, nullptr);
    CHECK(!result);
    CHECK(result.consumed() == example_crc);
    CHECK(result.state() == &state);
    CHECK(result.error_code() == (uint32_t)Errc::ethernet_crc_mismatch);
  }
}
